import { $set } from '@feng3d/serialization';
import { Clock, Mesh3D, MeshBasicMaterial3D, MeshStandardMaterial3D, Node3D, PerspectiveCamera3D, Plane, PlaneGeometry3D, PlaneHelper3D, Scene3D, ShadowMaterial3D, StatsComponent, TorusKnotGeometry3D, Vector3, WebGLRenderer3D } from 'feng3d-three';
import GUI from 'lil-gui';
import '../../extension/controls/OrbitControls3D';

let camera: PerspectiveCamera3D;
let scene: Scene3D;
let renderer: WebGLRenderer3D;
let object: Node3D;
let stats: StatsComponent;
let planes: Plane[];
let planeObjects: Mesh3D[];
let planeHelpers: PlaneHelper3D[];
let clock: Clock;

const params = {

    animate: true,
    planeX: {

        constant: 0,
        negated: false,
        displayHelper: false

    },
    planeY: {

        constant: 0,
        negated: false,
        displayHelper: false

    },
    planeZ: {

        constant: 0,
        negated: false,
        displayHelper: false

    }

};

init();
animate();

function createPlaneStencilGroup(geometry: TorusKnotGeometry3D, plane: Plane, renderOrder: number)
{
    const group = new Node3D();
    const baseMat = new MeshBasicMaterial3D();
    baseMat.depthWrite = false;
    baseMat.depthTest = false;
    baseMat.colorWrite = false;
    baseMat.stencilWrite = true;
    baseMat.stencilFunc = 'AlwaysStencilFunc';

    // back faces
    const mat0 = baseMat.clone();
    mat0.side = 'BackSide';
    mat0.clippingPlanes = [plane];
    mat0.stencilFail = 'IncrementWrapStencilOp';
    mat0.stencilZFail = 'IncrementWrapStencilOp';
    mat0.stencilZPass = 'IncrementWrapStencilOp';

    const mesh0 = new Node3D().addComponent('Mesh3D', { geometry, material: mat0 });
    mesh0.renderOrder = renderOrder;
    group.addChild(mesh0.entity);

    // front faces
    const mat1 = baseMat.clone();
    mat1.side = 'FrontSide';
    mat1.clippingPlanes = [plane];
    mat1.stencilFail = 'DecrementWrapStencilOp';
    mat1.stencilZFail = 'DecrementWrapStencilOp';
    mat1.stencilZPass = 'DecrementWrapStencilOp';

    const mesh1 = new Node3D().addComponent('Mesh3D', { geometry, material: mat1 });
    mesh1.renderOrder = renderOrder;

    group.addChild(mesh1.entity);

    return group;
}

function init()
{
    clock = new Clock();

    scene = new Node3D().addComponent('Scene3D');

    camera = new Node3D().addComponent('PerspectiveCamera3D', { fov: 36, aspect: window.innerWidth / window.innerHeight, near: 1, far: 100 });
    camera.entity.position.set(2, 2, 2);

    scene.addComponent('AmbientLight3D', { color: { value: 0xffffff }, intensity: 0.5 });

    const dirLight = new Node3D().addComponent('DirectionalLight3D', { color: { value: 0xffffff }, intensity: 1 });
    dirLight.entity.position.set(5, 10, 7.5);
    dirLight.castShadow = true;
    dirLight.shadow.camera.right = 2;
    dirLight.shadow.camera.left = -2;
    dirLight.shadow.camera.top = 2;
    dirLight.shadow.camera.bottom = -2;

    dirLight.shadow.mapSize.width = 1024;
    dirLight.shadow.mapSize.height = 1024;
    scene.entity.addChild(dirLight.entity);

    planes = [
        new Plane(new Vector3(-1, 0, 0), 0),
        new Plane(new Vector3(0, -1, 0), 0),
        new Plane(new Vector3(0, 0, -1), 0)
    ];

    planeHelpers = planes.map((p) => new Node3D().addComponent('PlaneHelper3D', { plane: p, size: 2, hex: 0xffffff }));
    planeHelpers.forEach((ph) =>
    {
        ph.entity.visible = false;
        scene.entity.addChild(ph.entity);
    });

    const geometry = $set(new TorusKnotGeometry3D(), { radius: 0.4, tube: 0.15, tubularSegments: 220, radialSegments: 60 });
    object = new Node3D();
    scene.entity.addChild(object);

    // Set up clip plane rendering
    planeObjects = [];
    const planeGeom = $set(new PlaneGeometry3D(), { width: 4, height: 4 });

    for (let i = 0; i < 3; i++)
    {
        const poGroup = new Node3D();
        const plane = planes[i];
        const stencilGroup = createPlaneStencilGroup(geometry, plane, i + 1);

        // plane is clipped by the other clipping planes
        const planeMat = $set(new MeshStandardMaterial3D(), {

            color: { value: 0xE91E63 },
            metalness: 0.1,
            roughness: 0.75,
            clippingPlanes: planes.filter((p) => p !== plane),

            stencilWrite: true,
            stencilRef: 0,
            stencilFunc: 'NotEqualStencilFunc',
            stencilFail: 'ReplaceStencilOp',
            stencilZFail: 'ReplaceStencilOp',
            stencilZPass: 'ReplaceStencilOp',

        });
        const po = new Node3D().addComponent('Mesh3D', { geometry: planeGeom, material: planeMat });
        po.onAfterRender = function (renderer)
        {
            renderer.clearStencil();
        };

        po.renderOrder = i + 1.1;

        object.addChild(stencilGroup);
        poGroup.addChild(po.entity);
        planeObjects.push(po);
        scene.entity.addChild(poGroup);
    }

    const material = $set(new MeshStandardMaterial3D(), {

        color: { value: 0xFFC107 },
        metalness: 0.1,
        roughness: 0.75,
        clippingPlanes: planes,
        clipShadows: true,
        shadowSide: 'DoubleSide',

    });

    // add the color
    const clippedColorFront = new Node3D().addComponent('Mesh3D', { geometry, material });
    clippedColorFront.castShadow = true;
    clippedColorFront.renderOrder = 6;
    object.addChild(clippedColorFront.entity);

    const ground = new Node3D().addComponent('Mesh3D', {
        geometry: $set(new PlaneGeometry3D(), { width: 9, height: 9, widthSegments: 1, heightSegments: 1 }),
        material: $set(new ShadowMaterial3D(), { color: { value: 0x000000 }, opacity: 0.25, side: 'DoubleSide' })
    });

    ground.entity.rotation.x = -Math.PI / 2; // rotates X/Y to X/Z
    ground.entity.position.y = -1;
    ground.receiveShadow = true;
    scene.entity.addChild(ground.entity);

    // Stats
    stats = scene.addComponent('StatsComponent');
    document.body.appendChild(stats.dom);

    // Renderer
    renderer = scene.addComponent('WebGLRenderer3D', { parameters: { antialias: true } });
    renderer.shadowMap.enabled = true;
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setClearColor(0x263238);
    window.addEventListener('resize', onWindowResize);
    document.body.appendChild(renderer.domElement);

    renderer.localClippingEnabled = true;

    // Controls
    const controls = scene.addComponent('OrbitControls3D', { camera, domElement: renderer.domElement });
    controls.minDistance = 2;
    controls.maxDistance = 20;
    controls.update();

    // GUI
    const gui = new GUI();
    gui.add(params, 'animate');

    const planeX = gui.addFolder('planeX');
    planeX.add(params.planeX, 'displayHelper').onChange((v) => planeHelpers[0].entity.visible = v);
    planeX.add(params.planeX, 'constant').min(-1).max(1).onChange((d) => planes[0].constant = d);
    planeX.add(params.planeX, 'negated').onChange(() =>
    {
        planes[0].negate();
        params.planeX.constant = planes[0].constant;
    });
    planeX.open();

    const planeY = gui.addFolder('planeY');
    planeY.add(params.planeY, 'displayHelper').onChange((v) => planeHelpers[1].entity.visible = v);
    planeY.add(params.planeY, 'constant').min(-1).max(1).onChange((d) => planes[1].constant = d);
    planeY.add(params.planeY, 'negated').onChange(() =>
    {
        planes[1].negate();
        params.planeY.constant = planes[1].constant;
    });
    planeY.open();

    const planeZ = gui.addFolder('planeZ');
    planeZ.add(params.planeZ, 'displayHelper').onChange((v) => planeHelpers[2].entity.visible = v);
    planeZ.add(params.planeZ, 'constant').min(-1).max(1).onChange((d) => planes[2].constant = d);
    planeZ.add(params.planeZ, 'negated').onChange(() =>
    {
        planes[2].negate();
        params.planeZ.constant = planes[2].constant;
    });
    planeZ.open();
}

function onWindowResize()
{
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate()
{
    const delta = clock.getDelta();

    requestAnimationFrame(animate);

    if (params.animate)
    {
        object.rotation.x += delta * 0.5;
        object.rotation.y += delta * 0.2;
    }

    for (let i = 0; i < planeObjects.length; i++)
    {
        const plane = planes[i];
        const po = planeObjects[i];
        plane.coplanarPoint(po.entity.position);
        po.entity.lookAt(
            po.entity.position.x - plane.normal.x,
            po.entity.position.y - plane.normal.y,
            po.entity.position.z - plane.normal.z,
        );
    }

    stats.begin();
    renderer.render(scene, camera);
    stats.end();
}
